package com.zoe.onelink.governance.gateway.route.publisher;

import cn.hutool.core.thread.ThreadUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.PropertyPreFilter;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.ConfigType;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.common.utils.MD5Utils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.google.common.base.Joiner;
import com.zoe.onelink.enhancer.init.ApplicationInitializer;
import com.zoe.onelink.governance.gateway.model.NativeGatewayRoute;
import com.zoe.onelink.governance.gateway.route.GatewayDynamicRouteGenerator;
import com.zoe.onelink.governance.gateway.serializer.GatewayRouteParamSerializer;
import com.zoe.onelink.governance.property.OnelinkGovernanceProperties;
import com.zoe.onelink.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationStartedEvent;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

/**
 * <p>标题: Spring Cloud Gateway 动态路由发布器</p>
 * <p>描述: 自动同步到 Nacos DataId: gateway-routes-dynamic.yaml GroupName: DYNAMIC_GEN</p>
 * <p>版权: Copyright (c) 2022</p>
 * <p>公司: 智业软件股份有限公司</p>
 *
 * @version: 1.0
 * @author: ruansheng
 * @date: 2022-10-18
 */
@Slf4j
public class GatewayDynamicRoutePublisher implements ApplicationInitializer {

    private final List<GatewayDynamicRouteGenerator> gatewayDynamicRouteGenerators;
    private final String serviceName;
    private final NacosConfigManager nacosConfigManager;
    private final OnelinkGovernanceProperties.Gateway.DynamicRoutePublish dynamicRouteProperties;
    private final ObjectMapper objectMapper;

    /**
     * 是否已处理
     */
    private final AtomicBoolean processed = new AtomicBoolean(false);
    private final PropertyPreFilter excludeMd5FieldFilter = (serializer, object, name) -> !"md5".equals(name);

    public GatewayDynamicRoutePublisher(List<GatewayDynamicRouteGenerator> gatewayDynamicRouteGenerators, String serviceName, NacosConfigManager nacosConfigManager, OnelinkGovernanceProperties.Gateway.DynamicRoutePublish dynamicRouteProperties) {
        this.gatewayDynamicRouteGenerators = gatewayDynamicRouteGenerators;
        this.serviceName = serviceName;
        this.nacosConfigManager = nacosConfigManager;
        this.dynamicRouteProperties = dynamicRouteProperties;
        if (ConfigType.YAML == dynamicRouteProperties.getConfigType()) {
            SimpleModule simpleModule = new SimpleModule()
                    .addSerializer(NativeGatewayRoute.GatewayRouteParam.class, new GatewayRouteParamSerializer());
            this.objectMapper = new YAMLMapper().registerModule(simpleModule);
        } else {
            this.objectMapper = new ObjectMapper();
        }
    }

    @Override
    public void init(ApplicationStartedEvent event) throws Exception {
        this.doPublish();
    }

    @Override
    public boolean isAsync() {
        return this.dynamicRouteProperties.isAsync();
    }

    public void doPublish() {
        List<NativeGatewayRoute> nativeGatewayRoutes = this.collectRoutes();
        List<NativeGatewayRoute> mergedRoutes = this.mergeRoutes(nativeGatewayRoutes);
        this.checkRoutePath(nativeGatewayRoutes, mergedRoutes);
        this.setRouteSign(mergedRoutes);
        int retryOnPublishFailureCount = Math.max(this.dynamicRouteProperties.getMaxRetryOnFailure(), 0);
        for (int i = 0; i < retryOnPublishFailureCount + 1; i++) {
            try {
                this.publishToNacos(mergedRoutes);
            } catch (Exception e) {
                log.error("[网关路由发布] 发布路由失败", e);
                if (i == retryOnPublishFailureCount - 1) {
                    String errMsg = StrUtil.format("[网关路由发布] 超过最大重试次数: {},已自动终止推送路由", retryOnPublishFailureCount);
                    throw new RuntimeException(errMsg);
                }
                ThreadUtil.sleep(this.dynamicRouteProperties.getRetryOnFailureWaitTime().toMillis());
                continue;
            }
            break;
        }

    }

    private void setRouteSign(List<NativeGatewayRoute> mergedRoutes) {
        for (NativeGatewayRoute mergedRoute : mergedRoutes) {
            // 计算实体类MD5之前保证MD5字段值为null
            mergedRoute.setMd5(this.getMd5(JSON.toJSONString(mergedRoute, this.excludeMd5FieldFilter)));
        }
    }

    private void checkRoutePath(List<NativeGatewayRoute> nativeGatewayRoutes, List<NativeGatewayRoute> mergedRoutes) {
        int nativeRoutePathCount = nativeGatewayRoutes.stream()
                .flatMap(route -> route.getPredicates().stream())
                .filter(predicatesDTO -> "Path".equalsIgnoreCase(predicatesDTO.getName()))
                .mapToInt(predicatesDTO -> predicatesDTO.getArgs().values().size())
                .sum();

        int mergedRoutePathCount = mergedRoutes.stream()
                .flatMap(route -> route.getPredicates().stream())
                .filter(predicatesDTO -> "Path".equalsIgnoreCase(predicatesDTO.getName()))
                .mapToInt(predicatesDTO -> predicatesDTO.getArgs().values().size())
                .sum();

        if (nativeRoutePathCount != mergedRoutePathCount) {
            log.warn("[网关路由发布] 原始路由数量[{}]与合并后的路由数量[{}]不一致,可能由于重复路由合并后导致,请自行确认该问题", nativeRoutePathCount, mergedRoutePathCount);
        }
    }

    private List<NativeGatewayRoute> mergeRoutes(List<NativeGatewayRoute> nativeGatewayRoutes) {
        Map<Boolean, List<NativeGatewayRoute>> routePartition = nativeGatewayRoutes.stream().collect(Collectors.groupingBy(route -> {
            List<NativeGatewayRoute.PredicatesDTO> predicates = route.getPredicates();
            // 断言数大于2处理
            if (predicates.size() > 2) {
                return false;
            }
            // 断言数正好为2且是Method与Path的组合
            if (predicates.size() == 2) {
                for (NativeGatewayRoute.PredicatesDTO predicate : predicates) {
                    if (!"Method".equalsIgnoreCase(predicate.getName()) && !"Path".equalsIgnoreCase(predicate.getName())) {
                        return false;
                    }
                }
                return true;
            }
            // 断言数为1时,必须是Path类型的匹配
            if (predicates.size() == 1) {
                return "Path".equalsIgnoreCase(predicates.get(0).getName());
            }
            return false;
        }));
        List<NativeGatewayRoute> needToProcessRoutes = routePartition.getOrDefault(Boolean.TRUE, Collections.emptyList());
        List<NativeGatewayRoute> noProcessRoutes = routePartition.getOrDefault(Boolean.FALSE, Collections.emptyList());
        List<NativeGatewayRoute> mergedRoutes = needToProcessRoutes.stream()
                .collect(Collectors.groupingBy(this::calcRuleHash))
                .entrySet().stream()
                .map(entry -> {
                    // 数量为1说明不存在重复项
                    if (entry.getValue().size() == 1) {
                        return entry.getValue().get(0);
                    }
                    // 获取所有Predicate中的Path
                    Set<String> paths = entry.getValue().stream()
                            .flatMap(route -> route.getPredicates().stream())
                            .filter(predicatesDTO -> "Path".equals(predicatesDTO.getName()))
                            .flatMap(predicatesDTO -> predicatesDTO.getArgs().values().stream())
                            .collect(Collectors.toSet());
                    // 多个路径根据逗号分割
                    String routePath = Joiner.on(',').join(paths);
                    // 构建断言器
                    NativeGatewayRoute.PredicatesDTO predicatesDTO = new NativeGatewayRoute.PredicatesDTO("Path=" + routePath);
                    // 获取原始路由
                    NativeGatewayRoute rawRoute = entry.getValue().get(0);
                    // 合并后的路由ID
                    String mergedRouteId = StrUtil.format("{}-mergedRoute-{}", serviceName, entry.getKey());
                    return new NativeGatewayRoute()
                            .setId(mergedRouteId)
                            .setUri(rawRoute.getUri())
                            .setFilters(rawRoute.getFilters())
                            .setPredicates(List.of(predicatesDTO))
                            .setOrder(rawRoute.getOrder())
                            .setMetadata(rawRoute.getMetadata());

                }).collect(Collectors.toList());
        mergedRoutes.addAll(noProcessRoutes);
        return mergedRoutes;
    }


    public int calcRuleHash(NativeGatewayRoute route) {
        return Objects.hash(route.getOrder(), route.getFilters(), route.getMetadata(), route.getUri());
    }

    /**
     * 收集所有路由
     */
    private List<NativeGatewayRoute> collectRoutes() {
        return this.gatewayDynamicRouteGenerators.stream()
                .flatMap(gatewayDynamicRouteGenerator -> {
                    List<NativeGatewayRoute> routes = gatewayDynamicRouteGenerator.generateNativeRoutes(this.serviceName);
                    if (routes == null) {
                        return null;
                    }
                    return routes.stream();
                }).filter(Objects::nonNull)
                .map(localRoute -> {
                    // 未开启安全模式直接返回
                    if (!this.dynamicRouteProperties.isSafetyMode()) {
                        return localRoute;
                    }
                    return this.resoleSafeRoute(localRoute) ? localRoute : null;
                    // 这里强制设置路由地址只能转发到自己的服务
                }).filter(Objects::nonNull)
                .peek(localRoute -> {
                    localRoute.setUri("lb://" + this.serviceName);
                    // 处理路径断言前缀
                    this.resolvePathPrefix(localRoute);
                    // 去重
                }).distinct()
                // 根据路由ID排序
                .sorted(Comparator.comparing(NativeGatewayRoute::getId))
                .collect(Collectors.toList());
    }

    /**
     * 处理路径断言前缀
     */
    private void resolvePathPrefix(NativeGatewayRoute localRoute) {
        String routePrefix = "/" + serviceName;
        for (NativeGatewayRoute.PredicatesDTO predicate : localRoute.getPredicates()) {
            if (!"Path".equals(predicate.getName())) {
                continue;
            }
            for (Map.Entry<String, String> e : predicate.getArgs().entrySet()) {
                String value = e.getValue();
                if (!value.startsWith(routePrefix)) {
                    e.setValue(routePrefix + value);
                }
            }
        }
    }

    /**
     * 检查是否是安全路由
     */
    private boolean resoleSafeRoute(NativeGatewayRoute localRoute) {
        // 安全检查
        List<NativeGatewayRoute.PredicatesDTO> predicates = localRoute.getPredicates();
        int predicateSize = predicates.size();
        // 没有配置断言器直接返回
        if (predicateSize == 0) {
            return false;
        }
        // 只有一条断言的情况
        if (predicateSize == 1) {
            NativeGatewayRoute.PredicatesDTO predicatesDTO = predicates.get(0);
            if ("Path".equals(predicatesDTO.getName())) {
                Map<String, String> args = this.resolvePredicateArgs(predicatesDTO, localRoute.getId());
                return !args.isEmpty();
            } else {
                this.appendPathPredicate(predicates);
                return true;
            }
        }
        // 没有路径断言
        boolean withoutPathPredicate = predicates.stream().noneMatch(predicatesDTO -> "Path".equals(predicatesDTO.getName()));
        if (withoutPathPredicate) {
            this.appendPathPredicate(predicates);
            return true;
        }
        return true;
    }

    /**
     * 添加服务路径断言
     */
    private void appendPathPredicate(List<NativeGatewayRoute.PredicatesDTO> predicates) {
        NativeGatewayRoute.PredicatesDTO pathPredicate = new NativeGatewayRoute.PredicatesDTO("Path=/" + this.serviceName + "/**");
        predicates.add(0, pathPredicate);
    }

    /**
     * 处理断言参数
     */
    private Map<String, String> resolvePredicateArgs(NativeGatewayRoute.PredicatesDTO predicatesDTO, String id) {
        Map<String, String> args = predicatesDTO.getArgs();
        List<String> removeKeys = args.entrySet().stream()
                .filter(e -> e.getValue().startsWith("/**") || e.getValue().startsWith("/" + this.serviceName + "/**"))
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());

        removeKeys.forEach(key -> {
            log.warn("[网关路由发布] 动态路由[{}]不允许使用'/**'开头的通配符,已自动移除路由[{}],如果想关闭此限制," +
                            "请配置[onelink.governance.gateway.safety-route-mode=false]",
                    id, args.get(key));
            args.remove(key);
        });
        return args;
    }

    /**
     * 发布路由
     */
    protected void publishToNacos(List<NativeGatewayRoute> localRoutes) throws NacosException, IOException {

        if (StrUtil.isBlank(this.serviceName)) {
            throw new IllegalArgumentException("Publish gateway routes failure: application serviceName could not be null");
        }

        String configDataId = this.dynamicRouteProperties.getConfigDataId();
        String configGroupName = this.dynamicRouteProperties.getConfigGroupName();
        ConfigType configType = this.dynamicRouteProperties.getConfigType();
        String configName = StrUtil.format("{}.{}", configDataId, configType.getType());

        ConfigService configService = this.nacosConfigManager.getConfigService();
        String remoteConfigContent = configService.getConfig(configName, configGroupName, 3000);
        String propertyPrefix = this.dynamicRouteProperties.getPrefix();
        String nodePtrExpr = "/" + propertyPrefix.replace(".", "/");
        // 远程配置不存在
        //noinspection AlibabaUndefineMagicConstant
        if (StrUtil.isBlank(remoteConfigContent) || "[]".equals(remoteConfigContent.replace(" ", ""))) {
            List<String> prefixNodeNames = StrUtil.split(propertyPrefix, StrUtil.DOT);
            ObjectNode configNode = this.objectMapper.createObjectNode();
            // 构建配置前缀节点
            this.createPrefixNodes(prefixNodeNames.iterator(), configNode);
            // 构建路由配置节点
            JsonNode routesNode = this.objectMapper.readTree(this.objectMapper.writeValueAsString(localRoutes));
            // 为了安全性考虑,这里强制最后一个节点名称为 routes
            ((ObjectNode) configNode.at(nodePtrExpr)).set("routes", routesNode);
            // 路由配置文本
            String routeConfig = this.objectMapper.writeValueAsString(configNode);
            // 发布返回结果
            boolean publishResult = configService.publishConfigCas(configName, configGroupName, routeConfig, null, configType.getType());
            if (publishResult) {
                log.info("[网关路由发布] 线上环境不存在路由配置文件,已自动创建配置[{}:{}],配置详情: {}", configName, configGroupName, routeConfig);
            } else {
                throw new RuntimeException("[网关路由发布] 使用CAS锁首次创建网关路由失败,路由配置: " + routeConfig);
            }
        } else {
            // 解析远程配置
            JsonNode remoteFullNode = this.objectMapper.readTree(remoteConfigContent);
            // 根据路由前缀解析路由节点
            JsonNode configNode = remoteFullNode.at(nodePtrExpr + "/routes");
            // 路由配置节点必须为数组类型
            if (configNode.isMissingNode() || !configNode.isArray()) {
                String errMsg = StrUtil.format("[网关路由发布] 配置节点[{}]与远程配置不匹配,为了避免配置混乱与互相覆盖问题," +
                        "请手动检查远程配置与本地路由前缀,远程配置详情:{}", nodePtrExpr, remoteConfigContent);
                throw new IllegalArgumentException(errMsg);
            }
            ArrayNode remoteRouteNodes = (ArrayNode) configNode;
            String uri = "lb://" + serviceName;
            // 远程路由列表
            List<NativeGatewayRoute> remoteRoutes = this.objectMapper
                    .readerForListOf(NativeGatewayRoute.class)
                    .<List<NativeGatewayRoute>>readValue(remoteRouteNodes)
                    .stream()
                    // 移除非本服务的路由配置
                    .filter(route -> uri.equals(route.getUri()))
                    .collect(Collectors.toList());
            // 比对本地路由与远程路由是否发生变更
            if (this.compareRoutes(localRoutes, remoteRoutes)) {
                log.info("[网关路由发布] 本地路由与线上路由一致,已自动忽略");
                return;
            }
            // 计算远程服务MD5
            String remoteConfigFullMd5 = this.getMd5(remoteConfigContent);
            Iterator<JsonNode> iterator = remoteRouteNodes.iterator();
            // 修改前先清理掉当前服务的路由配置
            while (iterator.hasNext()) {
                if (uri.equals(iterator.next().get("uri").asText())) {
                    iterator.remove();
                }
            }
            ArrayNode localArrayRouteNode = (ArrayNode) this.objectMapper.readTree(this.objectMapper.writeValueAsString(localRoutes));
            // 将本地路由配置加载到远程路由配置列表中
            remoteRouteNodes.addAll(localArrayRouteNode);
            String updatedRemoteRouteConfig = this.objectMapper.writeValueAsString(remoteFullNode);
            boolean result = configService.publishConfigCas(configName, configGroupName, updatedRemoteRouteConfig, remoteConfigFullMd5, configType.getType());
            if (result) {
                log.info("[网关路由发布] 服务[{}]路由已变更,更新线上网关路由配置成功,配置详情: {}", this.serviceName, updatedRemoteRouteConfig);
            } else {
                throw new RuntimeException("[网关路由发布] 使用CAS锁首次创建网关路由失败,路由配置: " + updatedRemoteRouteConfig);
            }
        }

    }

    /**
     * 这里使用MD5比对,因为路由内部的MAP结构不是稳定的
     */
    private boolean compareRoutes(List<NativeGatewayRoute> localRoutes, List<NativeGatewayRoute> remoteRoutes) {
        if (localRoutes.size() != remoteRoutes.size()) {
            return false;
        }

        return localRoutes.stream()
                .allMatch(localRoute -> remoteRoutes.stream()
                        .anyMatch(remoteRoute -> remoteRoute.getMd5().equals(localRoute.getMd5())));

    }

    /**
     * 获取文本MD5值
     */
    protected String getMd5(String text) {
        if (text == null) {
            text = "";
        }
        try {
            return MD5Utils.md5Hex(text.getBytes(StandardCharsets.UTF_8));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }


    /**
     * 创建前缀节点
     */
    public void createPrefixNodes(Iterator<String> nodeNameIt, ObjectNode parentNode) {
        if (!nodeNameIt.hasNext()) {
            return;
        }
        String next = nodeNameIt.next();
        ObjectNode childNode = this.objectMapper.createObjectNode();
        parentNode.set(next, childNode);
        createPrefixNodes(nodeNameIt, childNode);
    }

}
